/* * Copyright (c) 2011-2014 Julien Nicoulaud <julien.nicoulaud@gmail.com> * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package net.nicoulaj.idea.markdown.annotator; import com.intellij.lang.annotation.AnnotationHolder; import com.intellij.lang.annotation.ExternalAnnotator; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.colors.TextAttributesKey; import com.intellij.openapi.fileTypes.SyntaxHighlighter; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiFile; import com.intellij.psi.tree.IElementType; import net.nicoulaj.idea.markdown.highlighter.MarkdownSyntaxHighlighter; import net.nicoulaj.idea.markdown.settings.MarkdownGlobalSettings; import net.nicoulaj.idea.markdown.settings.MarkdownGlobalSettingsListener; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.pegdown.PegDownProcessor; import org.pegdown.ast.*; import java.util.HashSet; import java.util.Set; import static net.nicoulaj.idea.markdown.lang.MarkdownTokenTypes.*; /** * {@link ExternalAnnotator} responsible for syntax highlighting Markdown files. * <p/> * This is a hack to avoid implementing {@link com.intellij.lexer.Lexer}, * and directly use the AST from <a href="http://pegdown.org">pegdown</a>'s {@link PegDownProcessor} instead. * * @author Julien Nicoulaud <julien.nicoulaud@gmail.com> * @since 0.4 */ public class MarkdownAnnotator extends ExternalAnnotator<String, Set<MarkdownAnnotator.HighlightableToken>> { /** Logger. */ private static final Logger LOGGER = Logger.getInstance(MarkdownAnnotator.class); /** * The {@link com.intellij.openapi.fileTypes.SyntaxHighlighter} used by * {@link #apply(com.intellij.psi.PsiFile, java.util.Set, com.intellij.lang.annotation.AnnotationHolder)}. */ private static final SyntaxHighlighter SYNTAX_HIGHLIGHTER = new MarkdownSyntaxHighlighter(); /** The {@link PegDownProcessor} used for building the document AST. */ private ThreadLocal<PegDownProcessor> processor = initProcessor(); /** Init/reinit thread local {@link PegDownProcessor}. */ private static ThreadLocal<PegDownProcessor> initProcessor() { return new ThreadLocal<PegDownProcessor>() { @Override protected PegDownProcessor initialValue() { return new PegDownProcessor(MarkdownGlobalSettings.getInstance().getExtensionsValue(), MarkdownGlobalSettings.getInstance().getParsingTimeout()); } }; } /** Build a new instance of {@link MarkdownAnnotator}. */ public MarkdownAnnotator() { // Listen to global settings changes. MarkdownGlobalSettings.getInstance().addListener(new MarkdownGlobalSettingsListener() { public void handleSettingsChanged(@NotNull final MarkdownGlobalSettings newSettings) { initProcessor(); } }); } /** * Get the text source of the given file. * * @param file the {@link PsiFile} to process. * @return the file text. */ @Nullable @Override public String collectInformation(@NotNull PsiFile file) { return file.getText(); } /** * Collect {@link net.nicoulaj.idea.markdown.annotator.MarkdownAnnotator.HighlightableToken}s from the given file. * * @param source the source text to process. * @return a {@link Set} of {@link net.nicoulaj.idea.markdown.annotator.MarkdownAnnotator.HighlightableToken}s that should be used to do the file syntax highlighting. */ @Override public Set<HighlightableToken> doAnnotate(final String source) { final MarkdownASTVisitor visitor = new MarkdownASTVisitor(); try { processor.get().parseMarkdown(source.toCharArray()).accept(visitor); } catch (Exception e) { LOGGER.error("Failed processing Markdown document", e); } return visitor.getTokens(); } /** * Convert collected {@link net.nicoulaj.idea.markdown.annotator.MarkdownAnnotator.HighlightableToken}s in syntax highlighting annotations. * * @param file the source file. * @param annotationResult the {@link Set} of {@link net.nicoulaj.idea.markdown.annotator.MarkdownAnnotator.HighlightableToken}s collected on the file. * @param holder the annotation holder. */ @Override public void apply(final @NotNull PsiFile file, final Set<HighlightableToken> annotationResult, final @NotNull AnnotationHolder holder) { for (final HighlightableToken token : annotationResult) { final TextAttributesKey[] attrs = SYNTAX_HIGHLIGHTER.getTokenHighlights(token.getElementType()); if (attrs.length > 0) holder.createInfoAnnotation(token.getRange(), null).setTextAttributes(attrs[0]); } } /** * Describes a range of text that should be highlighted with a specific element type. * * @author Julien Nicoulaud <julien.nicoulaud@gmail.com> * @since 0.8 */ protected class HighlightableToken { /** The text range. */ protected final TextRange range; /** The associated element type. */ protected final IElementType elementType; /** * Build a new instance of {@link net.nicoulaj.idea.markdown.annotator.MarkdownAnnotator.HighlightableToken}. * * @param range the text range. * @param elementType the associated element type. */ public HighlightableToken(final TextRange range, final IElementType elementType) { this.range = range; this.elementType = elementType; } /** * Get the token text range. * * @return {@link #range} */ public TextRange getRange() { return range; } /** * Get the token element type. * * @return {@link #elementType} */ public IElementType getElementType() { return elementType; } } /** * {@link org.pegdown.ast.Visitor} used by {@link MarkdownAnnotator} to highlight a Markdown document. * * @author Julien Nicoulaud <julien.nicoulaud@gmail.com> * @since 0.4 */ protected class MarkdownASTVisitor implements Visitor { /** The collected token set. */ protected final Set<HighlightableToken> tokens = new HashSet<HighlightableToken>(20); /** * Get the collected tokens set. * * @return {@link #tokens} */ public Set<HighlightableToken> getTokens() { return tokens; } /** * Visit the {@link org.pegdown.ast.RootNode}. * * @param node the {@link org.pegdown.ast.RootNode} to visit */ public void visit(RootNode node) { for (AbbreviationNode abbreviationNode : node.getAbbreviations()) abbreviationNode.accept(this); for (ReferenceNode referenceNode : node.getReferences()) referenceNode.accept(this); visitChildren(node); } /** * Visit the {@link org.pegdown.ast.SimpleNode}. * * @param node the {@link org.pegdown.ast.SimpleNode} to visit */ public void visit(SimpleNode node) { switch (node.getType()) { case HRule: addToken(node, HRULE); break; case Apostrophe: case Ellipsis: case Emdash: case Endash: case Linebreak: case Nbsp: break; } } /** * Visit the {@link org.pegdown.ast.SuperNode}. * * @param node the {@link org.pegdown.ast.SuperNode} to visit */ public void visit(SuperNode node) { visitChildren(node); } /** * Visit the {@link org.pegdown.ast.ParaNode}. * * @param node the {@link org.pegdown.ast.ParaNode} to visit */ public void visit(ParaNode node) { visitChildren(node); } /** * Visit the {@link org.pegdown.ast.Node}. * <p/> * This method should never get called, highlights node as error. * * @param node the {@link org.pegdown.ast.Node} to visit */ public void visit(Node node) { addToken(node, ERROR_ELEMENT); } /** * Visit the {@link TextNode}. * * @param node the {@link TextNode} to visit */ public void visit(TextNode node) { addToken(node, TEXT); } /** * Visit the {@link SpecialTextNode}. * * @param node the {@link SpecialTextNode} to visit */ public void visit(SpecialTextNode node) { addToken(node, SPECIAL_TEXT); } /** * Visit the {@link StrikeNode}. * * @param node the {@link StrikeNode} to visit */ @Override public void visit(StrikeNode node) { addToken(node, STRIKETHROUGH); visitChildren(node); } /** * Visit the {@link StrongEmphSuperNode}. * * @param node the {@link StrongEmphSuperNode} to visit */ public void visit(StrongEmphSuperNode node) { addToken(node, node.isStrong() ? BOLD : ITALIC); visitChildren(node); } /** * Visit the {@link ExpImageNode}. * * @param node the {@link ExpImageNode} to visit */ public void visit(ExpImageNode node) { addToken(node, IMAGE); visitChildren(node); } /** * Visit the {@link ExpLinkNode}. * * @param node the {@link ExpLinkNode} to visit */ public void visit(ExpLinkNode node) { addToken(node, EXPLICIT_LINK); visitChildren(node); } /** * Visit the {@link RefLinkNode}. * * @param node the {@link RefLinkNode} to visit */ public void visit(final RefLinkNode node) { addToken(node, REFERENCE_LINK); visitChildren(node); } /** * Visit the {@link AutoLinkNode}. * * @param node the {@link AutoLinkNode} to visit */ public void visit(AutoLinkNode node) { addToken(node, AUTO_LINK); } /** * Visit the {@link MailLinkNode}. * * @param node the {@link MailLinkNode} to visit */ public void visit(MailLinkNode node) { addToken(node, MAIL_LINK); } /** * Visit the {@link HeaderNode}. * * @param node the {@link HeaderNode} to visit */ public void visit(HeaderNode node) { switch (node.getLevel()) { case 1: addToken(node, HEADER_LEVEL_1); break; case 2: addToken(node, HEADER_LEVEL_2); break; case 3: addToken(node, HEADER_LEVEL_3); break; case 4: addToken(node, HEADER_LEVEL_4); break; case 5: addToken(node, HEADER_LEVEL_5); break; case 6: addToken(node, HEADER_LEVEL_6); break; } visitChildren(node); } /** * Visit the {@link CodeNode}. * * @param node the {@link CodeNode} to visit */ public void visit(CodeNode node) { addToken(node, CODE); } /** * Visit the {@link VerbatimNode}. * * @param node the {@link VerbatimNode} to visit */ public void visit(VerbatimNode node) { addToken(node, VERBATIM); } /** * Visit the {@link WikiLinkNode}. * * @param node the {@link WikiLinkNode} to visit */ public void visit(WikiLinkNode node) { addToken(node, WIKI_LINK); } /** * Visit the {@link QuotedNode}. * * @param node the {@link QuotedNode} to visit */ public void visit(QuotedNode node) { addToken(node, QUOTE); } /** * Visit the {@link BlockQuoteNode}. * * @param node the {@link BlockQuoteNode} to visit */ public void visit(BlockQuoteNode node) { addToken(node, BLOCK_QUOTE); visitChildren(node); } /** * Visit the {@link BulletListNode}. * * @param node the {@link BulletListNode} to visit */ public void visit(BulletListNode node) { addToken(node, BULLET_LIST); visitChildren(node); } /** * Visit the {@link OrderedListNode}. * * @param node the {@link OrderedListNode} to visit */ public void visit(OrderedListNode node) { addToken(node, ORDERED_LIST); visitChildren(node); } /** * Visit the {@link ListItemNode}. * * @param node the {@link ListItemNode} to visit */ public void visit(ListItemNode node) { addToken(node, LIST_ITEM); visitChildren(node); } /** * Visit the {@link DefinitionListNode}. * * @param node the {@link DefinitionListNode} to visit */ public void visit(DefinitionListNode node) { addToken(node, DEFINITION_LIST); visitChildren(node); } /** * Visit the {@link DefinitionNode}. * * @param node the {@link DefinitionNode} to visit */ public void visit(DefinitionNode node) { addToken(node, DEFINITION); visitChildren(node); } /** * Visit the {@link DefinitionTermNode}. * * @param node the {@link DefinitionTermNode} to visit */ public void visit(DefinitionTermNode node) { addToken(node, DEFINITION_TERM); visitChildren(node); } /** * Visit the {@link TableNode}. * * @param node the {@link TableNode} to visit */ public void visit(TableNode node) { addToken(node, TABLE); visitChildren(node); } /** * Visit the {@link TableBodyNode}. * * @param node the {@link TableBodyNode} to visit */ public void visit(TableBodyNode node) { addToken(node, TABLE_BODY); visitChildren(node); } /** * Visit the {@link TableCellNode}. * * @param node the {@link TableCellNode} to visit */ public void visit(TableCellNode node) { addToken(node, TABLE_CELL); visitChildren(node); } /** * Visit the {@link TableColumnNode}. * * @param node the {@link TableColumnNode} to visit */ public void visit(TableColumnNode node) { addToken(node, TABLE_COLUMN); visitChildren(node); } /** * Visit the {@link TableHeaderNode}. * * @param node the {@link TableHeaderNode} to visit */ public void visit(TableHeaderNode node) { addToken(node, TABLE_HEADER); visitChildren(node); } /** * Visit the {@link TableRowNode}. * * @param node the {@link TableRowNode} to visit */ public void visit(TableRowNode node) { addToken(node, TABLE_ROW); visitChildren(node); } /** * Visit the {@link TableCaptionNode}. * * @param node the {@link TableCaptionNode} to visit */ public void visit(TableCaptionNode node) { addToken(node, TABLE_CAPTION); visitChildren(node); } /** * Visit the {@link HtmlBlockNode}. * <p/> * TODO: Real HTML support not implemented. * * @param node the {@link HtmlBlockNode} to visit */ public void visit(HtmlBlockNode node) { addToken(node, HTML_BLOCK); } /** * Visit the {@link InlineHtmlNode}. * <p/> * TODO: Real HTML support not implemented. * * @param node the {@link InlineHtmlNode} to visit */ public void visit(InlineHtmlNode node) { addToken(node, INLINE_HTML); } /** * Visit the {@link ReferenceNode}. * * @param node the {@link ReferenceNode} to visit */ public void visit(ReferenceNode node) { addToken(node, REFERENCE); visitChildren(node); } /** * Visit the {@link RefImageNode}. * * @param node the {@link RefImageNode} to visit */ public void visit(RefImageNode node) { addToken(node, REFERENCE_IMAGE); visitChildren(node); } /** * Visit the {@link AbbreviationNode}. * * @param node the {@link AbbreviationNode} to visit */ public void visit(AbbreviationNode node) { addToken(node, ABBREVIATION); visitChildren(node); } /** * Visit a {@link SuperNode}'s children. * * @param node the {@link Node} to visit */ protected void visitChildren(SuperNode node) { for (Node child : node.getChildren()) child.accept(this); } /** * Add the given {@link Node} to the set of highlightable tokens. * * @param node the {@link Node} to setup highlighting for * @param tokenType the {IElementType} to use for highlighting */ protected void addToken(Node node, IElementType tokenType) { if (node.getStartIndex() < node.getEndIndex()) tokens.add(new HighlightableToken(new TextRange(node.getStartIndex(), node.getEndIndex()), tokenType)); } } }